package charts.graphics;
import static com.google.common.base.Preconditions.checkNotNull;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.Collections;
import java.util.Map;
import org.jfree.text.TextUtilities;
import org.jfree.ui.TextAnchor;
import charts.Drawable;
import graphics.GraphUtils;
public class BeerCoasterV2 implements Drawable {
public static enum Condition {
NOT_EVALUATED("Not evaluated", Colors.NOT_EVALUATED),
VERY_GOOD("Very good", Colors.VERY_GOOD),
GOOD("Good", Colors.GOOD),
MODERATE("Moderate", Colors.MODERATE),
POOR("Poor", Colors.POOR),
VERY_POOR("Very poor", Colors.VERY_POOR);
private final String label;
private final Color color;
Condition(String label, Color color) {
this.label = label;
this.color = color;
}
public String getLabel() {
return label;
}
public Color getColor() {
return color;
}
}
public static enum Rotation {
CLOCKWISE, COUNTER_CLOCKWISE;
}
public static enum Indicator {
CHLOROPHYLL_A("Chlorophyll a", Category.WATER_QUALITY, 90, -60, Rotation.CLOCKWISE, 30),
TOTAL_SUSPENDED_SOLIDS("Total suspended solids", Category.WATER_QUALITY, 30, -60, Rotation.COUNTER_CLOCKWISE, 90),
COVER("Cover", Category.CORAL, -126, -24, Rotation.COUNTER_CLOCKWISE, 228),
ALGAE("Macroalgae", Category.CORAL, -102, -24, Rotation.COUNTER_CLOCKWISE, 204),
JUVENILE("Juvenile", Category.CORAL, -78, -24, Rotation.COUNTER_CLOCKWISE, 180),
SETTLEMENT("Change", Category.CORAL, -54, -24, Rotation.COUNTER_CLOCKWISE, 156),
COMPOSITION("Composition", Category.CORAL, -30, -24, Rotation.COUNTER_CLOCKWISE, 132),
ABUNDANCE("Abundance", Category.SEAGRASS, -150, -40, Rotation.CLOCKWISE, 260),
REPRODUCTION("Reproduction", Category.SEAGRASS, -190, -40, Rotation.CLOCKWISE, 300),
NUTRIENT_STATUS("Nutrient status", Category.SEAGRASS, -230, -40, Rotation.CLOCKWISE, 340),
;
private final String name;
private final Category category;
private final int startAngle;
private final int arcAngle;
private final Rotation textDirection;
private final int textAngle;
Indicator(String name, Category category, int startAngle, int arcAngle,
Rotation textDirection, int textAngle) {
this.name = name;
this.category = category;
this.startAngle = startAngle;
this.arcAngle = arcAngle;
this.textDirection = textDirection;
this.textAngle = textAngle;
}
public String getName() {
return name;
}
public Category getCategory() {
return category;
}
public int getStartAngle() {
return startAngle;
}
public int getArcAngle() {
return arcAngle;
}
public Rotation getTextDirection() {
return textDirection;
}
public int getTextAngle() {
return textAngle;
}
}
public static enum Category {
WATER_QUALITY("Water quality", 90, -120, Rotation.CLOCKWISE, 60, "waterquality.png"),
CORAL("Coral", -30, -120, Rotation.COUNTER_CLOCKWISE, 180, "coral.png"),
SEAGRASS("Seagrass", -150, -120, Rotation.CLOCKWISE, 300, "seagrass.png");
private String name;
private final int startAngle;
private final int arcAngle;
private final Rotation textDirection;
private final int textAngle;
Category(String name, int startAngle, int arcAngle, Rotation textDirection,
int textAngle, String imageName) {
this.name = name;
this.startAngle = startAngle;
this.arcAngle = arcAngle;
this.textDirection = textDirection;
this.textAngle = textAngle;
}
public String getName() {
return name;
}
public int getStartAngle() {
return startAngle;
}
public int getArcAngle() {
return arcAngle;
}
public Rotation getTextDirection() {
return textDirection;
}
public int getTextAngle() {
return textAngle;
}
}
private static final float BORDER_WIDTH = 1.5f;
private static final float CATEGORY_BORDER_WIDTH = 4.0f;
private static final Color CATEGORY_FONT_COLOR = Color.BLACK;
private static final Color INDICATOR_FONT_COLOR = Color.BLACK;
private static final Color LEGEND_FONT_COLOR = Color.BLACK;
private static final Font CATEGORY_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 18);
private static final Font INDICATOR_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
private static final Font LEGEND_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 18);
// The legend will be placed at the LEGEND_ANGLE measured from the center of the beer coaster
private static final int LEGEND_ANGLE = 79;
private final Dimension dimension = new Dimension(510,380);
private int bcx;
private int bcy;
private float bcr;
// indicator condition - NOT_EVALUATED by default
private final Condition[] iCondition = Collections.nCopies(
Indicator.values().length,
Condition.NOT_EVALUATED).toArray(new Condition[0]);
// category condition - NOT_EVALUATED by default
private final Condition[] cCondition = Collections.nCopies(
Category.values().length,
Condition.NOT_EVALUATED).toArray(new Condition[0]);
private Condition overall = Condition.NOT_EVALUATED;
private Map<Condition, Color> colors;
public BeerCoasterV2() {}
public BeerCoasterV2(Map<Condition, Color> colors) {
this.colors = colors;
}
public Condition getCondition(Indicator indicator) {
return iCondition[indicator.ordinal()];
}
public void setCondition(Indicator indicator, Condition condition) {
iCondition[indicator.ordinal()] = checkNotNull(condition);
}
public Condition getCondition(Category category) {
return cCondition[category.ordinal()];
}
public void setCondition(Category category, Condition condition) {
cCondition[category.ordinal()] = checkNotNull(condition);
}
public Condition getOverallCondition() {
return overall;
}
public void setOverallCondition(Condition overall) {
this.overall = overall;
}
private void drawArc(GraphUtils g, float radius, int startAngle, int arcAngle, Condition condition,
String label, float labelRadius, int labelAngle, Rotation labelDirection,
Color fc, GraphUtils.TextAnchor anchor) {
Stroke stroke = g.getGraphics().getStroke();
g.getGraphics().setColor(getColor(condition));
g.fillArc(bcx, bcy, radius, startAngle, arcAngle);
g.getGraphics().setColor(Color.WHITE);
g.getGraphics().setStroke(new BasicStroke(BORDER_WIDTH));
g.drawArc(bcx, bcy, radius, startAngle, arcAngle);
g.getGraphics().setColor(fc);
g.drawCircleText(label, bcx, bcy, labelRadius,
GraphUtils.toRadians(labelAngle),
labelDirection == Rotation.CLOCKWISE, anchor);
g.getGraphics().setStroke(stroke);
}
private void drawIndicator(GraphUtils g, Indicator indicator) {
g.getGraphics().setFont(INDICATOR_FONT);
FontMetrics fm = g.getGraphics().getFontMetrics();
drawArc(g, bcr, indicator.getStartAngle(), indicator.getArcAngle(),
getCondition(indicator), indicator.getName(), bcr - (fm.getDescent() + fm.getAscent()),
indicator.getTextAngle(), indicator.getTextDirection(),
INDICATOR_FONT_COLOR, GraphUtils.TextAnchor.BASELINE);
}
private void drawCategory(GraphUtils g, Category category) {
g.getGraphics().setFont(CATEGORY_FONT);
drawArc(g, bcr/3*2, category.getStartAngle(), category.getArcAngle(),
getCondition(category), category.getName(), bcr/2,
category.getTextAngle(), category.getTextDirection(),
CATEGORY_FONT_COLOR, GraphUtils.TextAnchor.CENTER);
}
private void drawCategoryBorder(GraphUtils g) {
Graphics2D g2d = g.getGraphics();
Stroke stroke = g2d.getStroke();
AffineTransform transform = g2d.getTransform();
g2d.setColor(Color.WHITE);
g2d.setStroke(new BasicStroke(CATEGORY_BORDER_WIDTH));
g2d.drawLine(bcx, bcy, bcx, (int)(bcy-bcr));
g2d.setTransform(AffineTransform.getRotateInstance(GraphUtils.toRadians(120), bcx, bcy));
g2d.drawLine(bcx, bcy, bcx, (int)(bcy-bcr));
g2d.setTransform(AffineTransform.getRotateInstance(GraphUtils.toRadians(240), bcx, bcy));
g2d.drawLine(bcx, bcy, bcx, (int)(bcy-bcr));
g2d.setStroke(stroke);
g2d.setTransform(transform);
}
private void drawLegend(GraphUtils g) {
g.getGraphics().setFont(LEGEND_FONT);
g.setColor(LEGEND_FONT_COLOR);
FontMetrics fm = g.getGraphics().getFontMetrics();
FontRenderContext frc = g.getGraphics().getFontRenderContext();
Point2D.Double ptSrc = new Point2D.Double(0,-(bcr+fm.getHeight()));
Point2D.Double ptDst = new Point2D.Double();
AffineTransform at = new AffineTransform();
at.concatenate(AffineTransform.getTranslateInstance(bcx, bcy));
at.concatenate(AffineTransform.getRotateInstance(
GraphUtils.toRadians(LEGEND_ANGLE)));
at.transform(ptSrc, ptDst);
float x = (float)ptDst.getX();
float y = (float)ptDst.getY();
for(Condition condition : Condition.values()) {
// We don't show "Not Evaluated" in the legend
if (condition == Condition.NOT_EVALUATED)
continue;
char ch[] = ("X "+condition.getLabel()).toCharArray();
GlyphVector gv = LEGEND_FONT.createGlyphVector(frc, ch);
for(int i=0;i<gv.getNumGlyphs();i++) {
Shape glyph;
if(i==0) {
glyph = gv.getGlyphVisualBounds(i);
g.setColor(getColor(condition));
} else {
glyph = gv.getGlyphOutline(i);
g.setColor(LEGEND_FONT_COLOR);
}
AffineTransform transform = new AffineTransform();
transform.concatenate(AffineTransform.getTranslateInstance(x, y));
glyph = transform.createTransformedShape(glyph);
g.getGraphics().fill(glyph);
}
y += fm.getHeight();
}
}
private void drawMarineCondition(GraphUtils g) {
g.setColor(getColor(getOverallCondition()));
g.fillCircle(bcx, bcy, bcr/3);
g.setStroke(CATEGORY_BORDER_WIDTH);
g.setColor(Color.white);
g.drawCircle(bcx, bcy, bcr/3);
g.setColor(CATEGORY_FONT_COLOR);
g.getGraphics().setFont(CATEGORY_FONT);
TextUtilities.drawAlignedString("Marine", g.getGraphics(), bcx, bcy, TextAnchor.BOTTOM_CENTER);
TextUtilities.drawAlignedString("condition", g.getGraphics(), bcx, bcy, TextAnchor.TOP_CENTER);
}
@Override
public void draw(Graphics2D graphics) {
Graphics2D g2d = (Graphics2D) graphics.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
GraphUtils g = new GraphUtils(g2d);
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, dimension.width, dimension.height);
g2d.setFont(CATEGORY_FONT);
FontMetrics fm = g.getGraphics().getFontMetrics();
// measured from original screenshoot
int radius = 188;
this.bcr = radius - fm.getHeight();
this.bcx = radius;
this.bcy = radius;
for(Indicator indicator : Indicator.values()) {
drawIndicator(g, indicator);
}
for(Category category : Category.values()) {
drawCategory(g, category);
}
drawCategoryBorder(g);
drawMarineCondition(g);
drawLegend(g);
g2d.dispose();
}
@Override
public Dimension getDimension(Graphics2D graphics) {
return new Dimension(dimension);
}
private Color getColor(Condition c) {
if((colors == null) || (colors.get(c) == null)) {
return c.getColor();
} else {
return colors.get(c);
}
}
}